<?php

declare(strict_types = 1);

namespace Tools\Helper;

/**
 * class InotifyMonitor
 *
 * @package Tools\Helper
 */
class InotifyMonitor
{
    /** @var int Need to monitor for event */
    const MONITOR_EVENT = \IN_MODIFY | \IN_CREATE | \IN_DELETE | \IN_DELETE_SELF | \IN_CLOSE_WRITE;

    /** @var string[] Event mask */
    const EVENT_MASK = [
        \IN_ACCESS => 'File was accessed (read)',
        \IN_MODIFY => 'File was modified',
        \IN_ATTRIB => 'Metadata changed',
        \IN_CLOSE_WRITE => 'File opened for writing was closed',
        \IN_CLOSE_NOWRITE => 'File not opened for writing was closed',
        \IN_OPEN => 'File was opened',
        \IN_MOVED_TO => 'File moved into watched directory',
        \IN_MOVED_FROM => 'File moved out of watched directory',
        \IN_CREATE => 'File or directory created in watched directory',
        \IN_DELETE => 'File or directory deleted in watched directory',
        \IN_DELETE_SELF => 'Watched file or directory was deleted',
        \IN_MOVE_SELF => 'Watch file or directory was moved',
        \IN_CLOSE => 'Equals to IN_CLOSE_WRITE | IN_CLOSE_NOWRITE',
        \IN_MOVE => 'Equals to IN_MOVED_FROM | IN_MOVED_TO',
        \IN_ALL_EVENTS => 'Bitmask of all the above constants',
        \IN_UNMOUNT => 'File system containing watched object was unmounted',
        \IN_Q_OVERFLOW => 'Event queue overflowed (wd is -1 for this event)',
        \IN_IGNORED => 'Watch was removed (explicitly by inotify_rm_watch() or because file was removed or filesystem unmounted',
        \IN_ISDIR => 'Subject of this event is a directory',
        \IN_ONLYDIR => 'Only watch pathname if it is a directory',
        \IN_DONT_FOLLOW => 'Do not dereference pathname if it is a symlink',
        \IN_MASK_ADD => 'Add events to watch mask for this pathname if it already exists',
        \IN_ONESHOT => 'Monitor pathname for one event, then remove from watch list.',
        1073741840 => 'High-bit: File not opened for writing was closed',
        1073741856 => 'High-bit: File was opened',
        1073742080 => 'High-bit: File or directory created in watched directory',
        1073742336 => 'High-bit: File or directory deleted in watched directory',
    ];

    /** @var array To save the resource returned by the inotify_init() */
    public $fds = [];
    /** @var array The file path used to save the monitoring */
    public $paths = [];
    /** @var array Save the monitoring descriptor returned by the inotify_add_watch() */
    public $watchDescriptionFd = [];
    /** @var int Timeout time */
    public $timeout = 3;

    /**
     * Add a monitored array of paths that can be a directory or a file.
     *
     * @param array $paths
     */
    public function __construct(array $paths = [])
    {
        if (!empty($paths)) {
            foreach ($paths as $path) {
                if (file_exists($path)) {
                    if (is_dir($path)) {
                        $this->addDirectory($path);
                    } else {
                        $this->addFile($path);
                    }
                }
            }
        }
    }

    /**
     * Destructor
     */
    public function __destruct()
    {
        if (!empty($this->fds)) {
            foreach ($this->fds as $fd) {
                fclose($fd);
            }
        }
    }

    /**
     * Add file monitoring.
     *
     * @param string $file
     * @return void
     */
    public function addFile(string $file)
    {
        $file = realpath($file);

        $this->watch($file);
    }

    /**
     * Add directory monitoring.
     *
     * @param string $directory
     * @return void
     */
    public function addDirectory(string $directory)
    {
        $directory = realpath($directory);
        if ($resource = opendir($directory)) {
            $this->watch($directory);

            while (($file = readdir($resource)) !== false) {
                if ($file == '.' || $file == '..') {
                    continue;
                }

                $file = $directory . DIRECTORY_SEPARATOR . $file;

                if (is_dir($file)) {
                    $this->addDirectory($file);
                }
            }

            closedir($resource);
        }
    }

    /**
     * Remove monitoring.
     *
     * @param $fid
     * @return void
     */
    public function remove($fid)
    {
        unset($this->paths[$fid]);

        fclose($this->fds[$fid]);

        unset($this->fds[$fid]);
    }

    /**
     * Run monitoring.
     *
     * @return mixed
     */
    public function run() : mixed
    {
        while (true) {
            $reads = $this->fds;
            $write = [];
            $except = [];

            if (stream_select($reads, $write, $except, $this->timeout) > 0) {
                if (!empty($reads)) {
                    foreach ($reads as $read) {
                        $events = inotify_read($read);
                        $fid = (int) $read;
                        $path = $this->paths[$fid];

                        foreach ($events as $event) {
                            $file = $path . DIRECTORY_SEPARATOR . $event['name'];
                            switch ($event['mask']) {
                                case IN_CREATE:
                                case 1073742080:
                                    if (is_dir($file)) {
                                        echo 'add ...', PHP_EOL;
                                        echo 'fid : ', $fid, PHP_EOL;
                                        $this->addDirectory($file);
                                    }
                                    break;
                                case IN_DELETE_SELF:
                                    if (is_dir($file)) {
                                        echo 'remove ...', PHP_EOL;
                                        echo 'fid : ', $fid, PHP_EOL;
                                        $this->remove($fid);

                                        $key = array_search($read, $reads);
                                        unset($reads[$key]);
                                    }
                                    break;
                            }
                            echo $event['name'], ' --- ', self::EVENT_MASK[$event['mask']], PHP_EOL;
                        }
                    }
                }
            } else {
                echo '------------------', PHP_EOL;
                echo 'current monitor path', PHP_EOL;
                print_r($this->paths);
                echo '------------------', PHP_EOL;
            }
        }
    }

    /**
     * @param bool|string $directory
     * @return void
     */
    public function watch(bool|string $directory) : void
    {
        $fd = inotify_init();

        if (is_bool($fd)) die('end'  . $directory);

        $fid = (int) $fd;

        $this->fds[$fid] = $fd;

        stream_set_blocking($this->fds[$fid], false);

        $this->paths[$fid] = $directory;

        $this->watchDescriptionFd[$fid] = inotify_add_watch($this->fds[$fid], $directory, self::MONITOR_EVENT);
    }
}